查看原文
其他

新型Linux/AES.DdoS IoT恶意软件 (part 1)

2017-12-13 StrokMitream 看雪学院


正如标题所言,该恶意软件是通过暴力破解SSH服务以及利用一系列IoT设备漏洞来散布流传的。该僵尸网络早在数年之前就流传于X86_64设备之间,现在它的目标有所转变。通过其输出符号表及其使用的C++ 结构分析,我们很容易知道Linux/AES.DdoS 是用C++ 编写的

      

将要用到的工具:

  • gdb-peda: https://github.com/longld/peda

  • BinaryNinja: https://binary.ninja/

  • ltrace: https://linux.die.net/man/1/ltrace

  • radare2: http://rada.re/r/


样本文件简略分析:

      

ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.14, not stripped.

    

好嘞,上面的两行表明:这是一个基于ARM 架构的32位的ELF文件,并且是经过静态链接生成的可执行文件。静态链接在IoT僵尸网络程序中十分常见,这样可以避免目标设备上缺少相关的库函数导致无法运行。


关于not stripped,首先了解一下stripped。Stripped是将程序中的符号表的信息剔除掉,这样子编译出来的可执行文件体积比较小;not stripped则反之,但是就是因为其保留了这些信息,所以便于调试。


MD5: 125679536fd4dd82106482ea1b4c1726

SHA1: 6caf6a6cf1bc03a038e878431db54c2127bfc2c1



ARM的相关准备工作



ARM是32位的体系架构,所以其寄存器也是32位宽的。在ARM架构的处理器中,在函数调用中通常是把函数参数读入r0 – r3。由于只用4个寄存器,如果函数参数超过4个,超出的参数将放置于堆栈中。ARM一共有15 个寄存器,但是都有特定的用途:

  • R0,r1,r2,r3 用来传递参数,r0通常用来保存子程序的返回值。

  • R4,r5,r6,r7 用来保存内部子程序的变量。

  • R10 保存当前堆栈的上下界。

  • R11 用来保存栈帧指针。

  • R12 也可以用于保存子程序的变量,但是并无法保证调用后其值不变。

  • R13 保存栈指针,即SP。

  • R14 称为子程序链接寄存器,用于保存子程序的返回地址。

  • R15 保存程序计数器。

      

太好了,现在,你就是ARM 专家了,那么可以继续下面的内容了......



深入探究main()函数



该恶意软件main()函数的初始入口地址为0x13DEC,我们用rabin2 找其入口:rabin2 –s kfts | grep “type=FUNC name=main” ,得到如下输出:

vaddr=0x00013dec paddr=0x0000bdec ord=5366 fwd=NONE sz=688 bind=GLOBAL type=FUNC name=main

 

好样的!可执行文件并未加壳。主函数执行后调用分支get_executable_name——通过readlink()函数读取/proc/self/exe 的文件符号链接。当在进程内部读取文件符号链接时,将返回可执行文件的运行位置地址。反汇编可以看到,我们创建std::string 类并将包含当前执行路径的char 数组复制到其中。这将稍后用于传递给持久化函数。



图01

      

接下来,我们检查程序是否在运行中或将其添加到启动列表。在check_running 进程,调用ps –e ,等待2s;然后从命令行获得输出,进而检查当前可执行程序是否在其中。如果是,执行exit 这支程序,返回0 (通常存于r0)迅速结束进程。如果未包含在其中,就执行接下来的持久化这一步。因此,如果已经运行过了,那么,很明显,就已经退出了进程。



图02



持久化


 

持久化是通过添加/etc/rc.local 和 /etc/init.d/boot.local 文件;当然,在写入这些文件之前,首先会检查这些文件是否已经存在。/etc/rc.local 将会在系统服务启动后执行指定的命令,只是其实现方法看起来有点像是外行生手所为:创建一系列shell 命令,调用system 函数执行这些命令。

      

接下来,一条字符串被格式化并调用 sed 程序写入文件。之后,作为参数传递给system 函数,正如上面所述。在这些字符串格式化操作中,使用的 buffer 是大小为300字节的虚拟地址0x9F48 段。从技术上来说,由于输入未被过滤,我们可以操控恶意代码的地址段并利用字符串格式化漏洞。这样,我们就可以操控堆栈,覆盖地址等等。这是该僵尸网络唯一使用的持续化手段。



图03



信息搜集


   

该程序的进程先fork自身,并调用setsid() 从父进程中脱离出来。所有从父进程中继承的文件描述符都将被关闭。下一步,创建一个线程——调用SendInfo 函数收集信息,例如系统CPU数目,网络连接速度,CPU 负载,本地网络适配器地址等等。

      

这个例程接下来调用子例程 get_occupy 。我们可以看到,它是通过遍历所有的CPU 来计算负载。从中,我们可以看到 r3 被用作循环计数器,当第一个操作数小于第二个操作数时,执行blt 指令这个分支。在X86架构下,这等效于 jle 指令。



图04

      

该恶意软件获取有关网络适配器信息是方法是读取/proc/net/dev 文件。它会从文件的开头处开始搜寻,并且对其进行语义分析,从而获得本地网络适配器的IP地址。

      

令我们感到十分奇怪的是,该恶意程序竟然会产生一些毫无实际意义的数据。比如,它会生成一个随机值,并将其作为网速。在子fake_net_speed 例程中,srandom 从 time 函数取种。我们把‘0’传递给 time 函数 (通常,是一个time_t 的的指针作为参数传递给该函数 )来验证其流程。之后,把 time 函数的值 存入 r3 ,又将r3 的值重新存入 r0 作为 srandom 的参数,这看起来像是编译器有些莫名其妙的问题。



图05

      

该调用产生的值作为 sprintf 声明而创建一个以 MB/s 为单位的当前网络连接速度的描述字符串。这显得相当奇怪,该恶意软件竟然产生一个虚假的网速信息……

      

这些数值随后由该子例程发送给C2 ……



通信过程初始化


       

在经过前面这些操作之后,我们最终来到最主要的核心部分——连接到C2 网络及接收控制命令的部分。ConnectServer(位置为0xCA1C)进程在函数的主体部分被调用,执行该函数后,来到返回套接字给C2服务器的ServerConnectCli(位置0xB5BC)分支,该套接字随后被赋值给全局变量 MainSocket 。既然这样,就来研究研究 ServerConnectCli ……

     

ServerConnectCli 首先创建一个TCP协议的套接字,如果该操作不成功,则该汇编分支转到另一个位于0xB654的子程序,调用perror()函数来显示一个用户友好的错误信息。



图06

     

用C语言描述,就是这样:

r0 = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

      

其将要连接的端口被编写者部分混淆了,初始值被加载到r3 寄存器。紧跟其后的,是对该值左移 16(0x10)位(下面反汇编代码的 lsl 指令),接着向右移16 位。这可能是编译器添加的,通过下面的反汇编代码,可以清楚地看到该过程:

mov r3, r0
strh r3, [r3, #0x104]
lsl r3, r3, #0x10 // shift r3 RIGHT by 0x10
lrl r3, r3, #0x10 // shift r3 LEFT by 0x10
mov r0, r3
bl htons


用伪代码表示,大概是这样:

r3 = ((r3 << 0x10) >> 0x10)

      

在分析中,我们发现在0xB1BB处有一个 AnalysisAddress 字符,咋一看,估计会吸引不少研究者的注意力。不过,这个子程序做的只是设置一个在后续连接中将会用到的 hostent 结构(    //http://man7.org/linux/man-pages/man3/gethostbyname.3.html)其将在0xC1FC8 处的值61.147.91.53作为第一个参数传递给r0 寄存器,把返回值 通过 gethostbyname 送入r3 处,之后,进行算术运算。

      

该恶意软件将调用 setsocket ,第一次是传送 IP_DROP_SOURCE_MEMBERSHIP 标志;上述步骤完成后,其再次调用 connect 进行与C2 服务器连接的初始化。进而,混合调用select 和 getsockopt 套接字来确保非阻塞套接字已经成功连接。如若连接不成功,则关闭套接字并退出。

      

一旦正常运行的套接字从 ServerConnectCli 返回,全局变量的值马上便被赋予该返回值。僵尸程序就会从感染的设备获取更多的数据。首先,它会从执行环境中抓取用户名,保存在 r11 中。假如调用 ‘uname’失败,那么就用‘Unknow’代替uname 字段。成功读取的话,就进入 0xCA8C 。



图07

      

通过 GetCpuInfo 函数,感染主机的更多信息可以搜集到。我随后慢慢道来:虚拟文件/proc/cpuinfo 是开放读取权限的,所以可以大块大块地读取直到EOF(End of File),然后,调用fclose () 释放文件。CPU的数目以及时钟频率也同样可以这样获得。

      

该程序还调用 sysinfo() 并且读取位于 r3 处的结构体。检查发现结构体变量的成员有交换分区的大小、RAM大小等些元素,这些都将格式化输出到字符串中。

      

在控制流中,可以看到,如果通过 ‘MainSocket’发送消息失败,那么,将会进入另一分支并且关闭套接字。



图08

      

对于那些不懂ARM 架构的, beq 指令是指当比较的结果为0,即相等时,则跳转到0xCBD4 。该支程序仅是简单地关闭套接字。接下来是 select ,如果调用成功,则读取来自C2 的数据;不过,在此之前,会先清空 buffer ,并获得最大 0x1380 的空间用来存放数据。同样的,如果该步骤失败,则进入另一分支,关闭套接字同时清空数据。



小结


      

到目前为止,我们可以看到,该恶意程序的创作者在网络编程方面经验丰富。作者熟悉Pascal 语言风格(例如: LikeThis)以及命名如GetCpuInfo ———可以推测作者过去或许从事Windows开发。

    

这是本小节的结尾,我们将会在后面更加深入地分析其攻击手段和研究该bot 可执行的其他控制命令。





本文由看雪翻译小组 StrokMitream 编译

转载请注明来自看雪社区


热门阅读


点击阅读原文/read,

更多干货等着你~


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存